#pragma once

#include <string>
#include <unordered_map>
#include "thread.h"
#include <Windows.h>
#include "str_utils.h"

#define FILE_ACTION_CUT_PASTE		0x00000006
#define FILE_ACTION_COPY_PASTE		0x00000007
#define FILE_ACTION_SAVE_AS			0x00000008

inline std::wstring ExtractFilename(const std::wstring& fullFilename)
{
	const size_t idx = fullFilename.find_last_of(L"/\\:");
	if (idx == std::wstring::npos)
		return fullFilename;
	else
		return fullFilename.substr(idx + 1);
}

inline std::wstring ExtractFilePath(const std::wstring& fullFilename)
{
	const size_t idx = fullFilename.find_last_of(L"/\\:");
	if (idx == std::wstring::npos)
		return std::wstring();
	else
		return fullFilename.substr(0, idx + 1);
}

inline void RemovePathTrim(IN OUT std::wstring& path)
{
	size_t count = 0;
	for (auto it = path.rbegin(); it != path.rend() && (*it == '\\' || *it == '/'); ++it)
		++count;
	if (count > 0)
		path.erase(path.size() - count, count);
}

inline bool IsExisted(const std::wstring& fullFilename)
{
	const DWORD attr = GetFileAttributesW(fullFilename.c_str());
	return (INVALID_FILE_ATTRIBUTES != attr);

	/*
    WIN32_FIND_DATA Win32_Find_Data;  
    HANDLE hFindFile;  
  
	hFindFile = FindFirstFileW(fullFilename.c_str(), &Win32_Find_Data);  
  
    if (INVALID_HANDLE_VALUE == hFindFile)  
    {  
        return false;  
    }  
    else  
    {  
        FindClose(hFindFile);  
        return true;  
    }
	*/
}

inline bool IsTemporary(const std::wstring& fullFilename, bool isDirectory)
{
	if (!isDirectory)
	{
		const size_t idx = fullFilename.find_last_of(L"./\\");
		if (idx == std::wstring::npos || fullFilename[idx] != '.')
			return true;
	}

	if (EndWithI(fullFilename, L".tmp", 4))
		return true;

	for (int retryCount = 0; ; ++retryCount)
	{
		const DWORD attr = GetFileAttributesW(fullFilename.c_str());
		if (INVALID_FILE_ATTRIBUTES != attr)
		{
			if (attr & (FILE_ATTRIBUTE_TEMPORARY | FILE_ATTRIBUTE_HIDDEN))
				return true;
		}

		if (retryCount >= 10)
			break;

		SwitchToThread();
	}

	return false;
}

inline bool IsDirectory(const std::wstring& fullFilename)
{
	const DWORD attr = GetFileAttributesW(fullFilename.c_str());
	if (INVALID_FILE_ATTRIBUTES != attr)
	{
		if (attr & FILE_ATTRIBUTE_DIRECTORY)
			return true;
	}

	return false;
}

inline bool GetCurrentWindow(OUT HWND& wnd)
{
	wnd = NULL;
	const HWND wndFG = GetForegroundWindow();
	if (wndFG == NULL)
		return false;

	const HWND wndShell = GetShellWindow();
	
	DWORD wndProcessID, wndShellProcessID;
	GetWindowThreadProcessId(wndFG, &wndProcessID);
	GetWindowThreadProcessId(wndShell, &wndShellProcessID);
	if (wndProcessID == wndShellProcessID)
		return true;	// L"Windows Explorer";

	wnd = wndFG;
	for (;;)
	{
		HWND wndOwner = GetWindow(wnd, GW_OWNER);
		if (wndOwner == NULL)
			break;
		wnd = wndOwner;
	}
	return (wndFG == wnd);
}

inline std::wstring GetWindowText(HWND wnd)
{
	wchar_t buf[4 * 1024];
	int len = InternalGetWindowText(wnd, buf, _countof(buf));
	if (len <= 0)
	{
		len = swprintf_s(buf, L"[HWND %X]", wnd);
	}
	return std::wstring(buf, len);
}

inline void ExtractAppAndDoc(std::wstring&& text, OUT std::wstring& app, OUT std::wstring& doc)
{
	if (text.empty())
		return;
		
	const size_t pos = text.rfind(L" - ");
	if (pos == std::wstring::npos)
	{
		app = std::move(text);
	}
	else if (text.find('.', pos + 3) == std::wstring::npos)
	{
		doc = text.substr(0, pos);
		text.erase(text.begin(), text.begin() + pos + 3);
		app = std::move(text);
	}
	else
	{
		app = text.substr(0, pos);
		text.erase(text.begin(), text.begin() + pos + 3);
		doc = std::move(text);
	}
}

bool GetCurrentAppAndDoc(OUT std::wstring& app, OUT std::wstring& doc)
{
	HWND wnd;
	const bool ownerless = GetCurrentWindow(wnd);
	if (wnd != NULL)
		ExtractAppAndDoc(GetWindowText(wnd), app, doc);
	return ownerless;
}

template <typename ACTION>
void CheckCurrentWndWithAppAndDoc(ACTION&& action)
{
	std::thread([=](){
		for (;;)
		{
			std::wstring app, doc;
			HWND wnd;
			const bool ownerless = GetCurrentWindow(wnd);
			if (wnd != NULL)
				ExtractAppAndDoc(GetWindowText(wnd), app, doc);

			action(wnd, std::move(app), std::move(doc), ownerless);

			Sleep(50);
		}
	});
}

template <typename ACTION>
void DetectCtrlXCVWithoutRepeat(ACTION&& action)
{
	std::thread([=](){
		SHORT xLastState = -1;
		SHORT cLastState = -1;
		SHORT vLastState = -1;
		for (;;)
		{
			if (GetAsyncKeyState(VK_MENU) >= 0 && GetAsyncKeyState(VK_CONTROL) < 0)
			{
				const SHORT xState = GetAsyncKeyState('X');
				const SHORT cState = GetAsyncKeyState('C');
				const SHORT vState = GetAsyncKeyState('V');
				std::wstring app, doc;
				bool ownerless = false;
				bool extracted = false;
				if (xState < 0 && xLastState >=0)
				{
					if (!extracted)
					{
						ownerless = GetCurrentAppAndDoc(app, doc);
						extracted = true;
					}
					action('X', (const std::wstring&)app, (const std::wstring&)doc, (const bool)ownerless);
				}
				if (cState < 0 && cLastState >=0)
				{
					if (!extracted)
					{
						ownerless = GetCurrentAppAndDoc(app, doc);
						extracted = true;
					}
					action('C', (const std::wstring&)app, (const std::wstring&)doc, (const bool)ownerless);
				}
				if (vState < 0 && vLastState >=0)
				{
					if (!extracted)
					{
						ownerless = GetCurrentAppAndDoc(app, doc);
						extracted = true;
					}
					action('V', (const std::wstring&)app, (const std::wstring&)doc, (const bool)ownerless);
				}

				xLastState = xState;
				cLastState = cState;
				vLastState = vState;
				Sleep(5);
			}
			else
			{
				xLastState = -1;
				cLastState = -1;
				vLastState = -1;
				Sleep(10);
			}
		}
	});
}

template <typename ACTION>
void DetectCtrlXCVWithRepeat(ACTION&& action)
{
	std::thread([=](){
		SHORT xLastState = 0;
		SHORT cLastState = 0;
		SHORT vLastState = 0;
		for (;;)
		{
			const SHORT xState = GetAsyncKeyState('X');
			const SHORT cState = GetAsyncKeyState('C');
			const SHORT vState = GetAsyncKeyState('V');
			if (GetAsyncKeyState(VK_MENU) >= 0 && GetAsyncKeyState(VK_CONTROL) < 0)
			{
				std::wstring app, doc;
				bool ownerless = false;
				bool extracted = false;
				if ((xState < 0 && xLastState >=0) || (xState & 1))
				{
					if (!extracted)
					{
						ownerless = GetCurrentAppAndDoc(app, doc);
						extracted = true;
					}
					action('X', (const std::wstring&)app, (const std::wstring&)doc, (const bool)ownerless);
				}
				if ((cState < 0 && cLastState >=0) || (cState & 1))
				{
					if (!extracted)
					{
						ownerless = GetCurrentAppAndDoc(app, doc);
						extracted = true;
					}
					action('C', (const std::wstring&)app, (const std::wstring&)doc, (const bool)ownerless);
				}
				if ((vState < 0 && vLastState >=0) || (vState & 1))
				{
					if (!extracted)
					{
						ownerless = GetCurrentAppAndDoc(app, doc);
						extracted = true;
					}
					action('V', (const std::wstring&)app, (const std::wstring&)doc, (const bool)ownerless);
				}
				Sleep(5);
			}
			else
				Sleep(10);
			xLastState = xState;
			cLastState = cState;
			vLastState = vState;
		}
	});
}

struct WndWithAppAndDoc
{
	HWND Wnd;
	std::wstring App;
	std::wstring Doc;
	bool Ownerless;
};

template <typename ACTION>
void DetectFileChange(const std::wstring dir, const WndWithAppAndDoc* pHistory, size_t count,volatile size_t* pNextIndex, ACTION&& action)
{
	std::thread([=](){
		//open the directory to watch....
		const HANDLE hDir = CreateFileW(dir.c_str(), 
										FILE_LIST_DIRECTORY, 
										FILE_SHARE_READ | FILE_SHARE_WRITE ,//| FILE_SHARE_DELETE, <-- removing FILE_SHARE_DELETE prevents the user or someone else from renaming or deleting the watched directory. This is a good thing to prevent.
										NULL, //security attributes
										OPEN_EXISTING,
										FILE_FLAG_BACKUP_SEMANTICS //<- the required priviliges for this flag are: SE_BACKUP_NAME and SE_RESTORE_NAME.  CPrivilegeEnabler takes care of that.
										// | FILE_FLAG_OVERLAPPED //OVERLAPPED!
										, NULL);
		if( hDir == INVALID_HANDLE_VALUE )
		{
			const DWORD dwError = GetLastError();
			wprintf_s(L"Error: Couldn't open directory '%s' for monitoring. Error Code: %d\n", dir.c_str(), dwError);
		}

#ifdef _DEBUG
		for (int groupID = 0;; ++groupID)
#else
		for (;;)
#endif
		{
			char notifyBuf[4 * 1024];
			DWORD bytesReturned;
			if (!ReadDirectoryChangesW(hDir, 
									  notifyBuf, sizeof(notifyBuf), 
									  TRUE, 
									  FILE_NOTIFY_CHANGE_FILE_NAME 
									  | FILE_NOTIFY_CHANGE_DIR_NAME
									  //| FILE_NOTIFY_CHANGE_ATTRIBUTES
									  | FILE_NOTIFY_CHANGE_LAST_WRITE 
									  //| FILE_NOTIFY_CHANGE_LAST_ACCESS
									  | FILE_NOTIFY_CHANGE_CREATION,
									  &bytesReturned,
									  NULL,
									  NULL))
			{
				const DWORD dwError = GetLastError();
				wprintf_s(L"Error: Couldn't read directory '%s' changes. Error Code: %d\n", dir.c_str(), dwError);
			}

			const size_t curIndex = *pNextIndex + (count - 10);
			std::wstring app, doc, old;
			HWND wnd;
			const bool ownerless = GetCurrentWindow(wnd);
			if (wnd != NULL)
			{
				for (size_t index = curIndex; ; --index)
				{
					const WndWithAppAndDoc& item = pHistory[index % count];
					if (item.Wnd == wnd)
					{
						app = item.App;
						doc = item.Doc;
						break;
					}

					if (curIndex - index >= 10) 
					{
						ExtractAppAndDoc(GetWindowText(wnd), app, doc);
						break;
					}
				}
			}

#ifdef _DEBUG
			wprintf_s(L"  GroupID %d\n", groupID);
#endif

			DWORD lastAction = 0;
			std::wstring lastModifiedFilename;
			for (const char* it = notifyBuf; it < notifyBuf + bytesReturned; it += ((FILE_NOTIFY_INFORMATION*)it)->NextEntryOffset)
			{
				const FILE_NOTIFY_INFORMATION& notifyInfo = *(const FILE_NOTIFY_INFORMATION*)it;
				std::wstring filename((const wchar_t*)&notifyInfo.FileName, notifyInfo.FileNameLength / 2);

				if (notifyInfo.Action == FILE_ACTION_RENAMED_OLD_NAME)
					old = std::move(filename);
				else if (notifyInfo.Action == FILE_ACTION_RENAMED_NEW_NAME)
					action(notifyInfo.Action, std::move(filename), (const std::wstring&)app, (const std::wstring&)old, ownerless);
				else if (notifyInfo.Action == FILE_ACTION_MODIFIED)
				{
					if (lastAction != FILE_ACTION_MODIFIED || lastModifiedFilename != filename)
					{
						lastModifiedFilename = filename;
						action(notifyInfo.Action, std::move(filename), (const std::wstring&)app, (const std::wstring&)doc, ownerless);
					}
#ifdef _DEBUG
					else
						wprintf_s(L"  remove duplicated FILE_ACTION_MODIFIED.\n");
#endif
				}
				else if (notifyInfo.Action == FILE_ACTION_REMOVED)
					action(notifyInfo.Action, std::wstring(), (const std::wstring&)app, (const std::wstring&)filename, ownerless);
				else
					action(notifyInfo.Action, std::move(filename), (const std::wstring&)app, (const std::wstring&)doc, ownerless);

				lastAction = notifyInfo.Action;

				if (notifyInfo.NextEntryOffset <= 0)
					break;
			}
		}
	});
}

